#include "terrainrenderer.h"

#include <algorithm>
#include <iostream>
#include <math.h>
#include <set>

#include "entities/terraintype.h"
#include "entities/tile.h"

#include "primitives/graph.h"

#include "renderer/shader.h"

#include "resourcemanager.h"

TerrainRenderer::TerrainRenderer(int width, int height, float frustumScale, float zNear, float zFar):SubRenderer(width, height, frustumScale, zNear, zFar), num_river_segments(0), num_road_segments(0){
    std::vector<std::string> unfList1 {"camera", "position", "perspectiveMatrix", "angle"};
    std::vector<std::string> unfList2 {"camera", "position", "perspectiveMatrix", "angle"};
    InitialiseShaderProgram("terrain", unfList1);
    InitialiseShaderProgram("terrain_picking", unfList2);
}

void TerrainRenderer::LoadTerrain(){
    std::vector<TerrainType*> terrains = resourceManager->GetAllTerrainTypes();
    std::vector<std::string> textures;
    for(auto terrain:terrains){
        textures.push_back(terrain->GetTexture());
    }
    LoadTerrainTextures(textures);
    LoadTerrainMesh();

    LoadRivers();
    LoadRoads();
    LoadTerritoryEdges();
}

void TerrainRenderer::LoadTerrainTextures(std::vector<std::string> textureFileLocs){
    GLFWimage tempImage;
    std::vector<unsigned char> superData;
    int height = 0;
    int width = 0;
    int index = 0;
//    int count = 0;

    ///TODO This will probably get slower with more terrain typess
    for(auto location:textureFileLocs){
        glfwReadImage((std::string("data/terrain/")+location).c_str(), &tempImage, GLFW_BUILD_MIPMAPS_BIT);
        if(tempImage.Width==0){
            continue;
        }
        superData.insert(superData.end(), tempImage.Data, tempImage.Data + tempImage.Width*tempImage.Height*tempImage.BytesPerPixel);
        height+=tempImage.Height;
        width = tempImage.Width > width ? tempImage.Width : width;

        glfwFreeImage(&tempImage);

        std::string name = location.substr(0, location.find('.'));
        textureIndices.insert(std::pair<std::string,int>(name,index));
        index++;
    }
//    std::cout<<"superdata.push_back used: "<<count<<std::endl;

    glGenTextures(1, &superTexture);
    glBindTexture(GL_TEXTURE_2D, superTexture);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, &superData[0]);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glBindTexture(GL_TEXTURE_2D, 0);
}

void TerrainRenderer::LoadTerrainMesh(){
    std::vector<Tile*> tiles = gamestate->GetAllTiles();
    std::vector<float> vertices;
    std::vector<float> normals;
    std::vector<float> uvs;
    std::vector<float> colors;
    float subTexHeight = 1.0f/textureIndices.size()-0.01;

    GLuint verticesVBO;
    GLuint pickverticesVBO;
    GLuint normalsVBO;
    GLuint colorsVBO;
    GLuint uvsVBO;

    for(auto tile:tiles){
        int x = tile->Getx();
        int y = tile->Gety();
        float offset = (float)(textureIndices.find(tile->GetTerrainType())->second)/textureIndices.size()+0.005;

        vertices.push_back(x-0.5);
        vertices.push_back(y-0.5);
        vertices.push_back(0);
        vertices.push_back(1);

        vertices.push_back(x+0.5);
        vertices.push_back(y-0.5);
        vertices.push_back(0);
        vertices.push_back(1);

        vertices.push_back(x+0.5);
        vertices.push_back(y+0.5);
        vertices.push_back(0);
        vertices.push_back(1);

        vertices.push_back(x-0.5);
        vertices.push_back(y+0.5);
        vertices.push_back(0);
        vertices.push_back(1);

        for(unsigned int i=0;i<4;i++){
            normals.push_back(0);
            normals.push_back(0);
            normals.push_back(1);
            normals.push_back(1);
        }

        uvs.push_back(0);
        uvs.push_back(offset);
        uvs.push_back(1);
        uvs.push_back(offset);
        uvs.push_back(1);
        uvs.push_back(offset+subTexHeight);
        uvs.push_back(0);
        uvs.push_back(offset+subTexHeight);

        // Repeated four times, once for each vertex in a square
        for(unsigned int i=0; i<4; i++){
            colors.push_back(tile->fRed());
            colors.push_back(tile->fGreen());
            colors.push_back(tile->fBlue());
            colors.push_back(1.0f);
        }
    }

    glGenVertexArrays(1, &terrainDrawVAO);
    glBindVertexArray(terrainDrawVAO);

    // @TODO DRY
    glGenBuffers(1, &verticesVBO);
    glBindBuffer(GL_ARRAY_BUFFER, verticesVBO);
    glBufferData(GL_ARRAY_BUFFER, vertices.size()*sizeof(float), &vertices[0], GL_STATIC_DRAW);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, 0);
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    glGenBuffers(1, &normalsVBO);
    glBindBuffer(GL_ARRAY_BUFFER, normalsVBO);
    glBufferData(GL_ARRAY_BUFFER, normals.size()*sizeof(float), &normals[0], GL_STATIC_DRAW);
    glEnableVertexAttribArray(1);
    glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 0, 0);
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    glGenBuffers(1, &uvsVBO);
    glBindBuffer(GL_ARRAY_BUFFER, uvsVBO);
    glBufferData(GL_ARRAY_BUFFER, uvs.size()*sizeof(float), &uvs[0], GL_STATIC_DRAW);
    glEnableVertexAttribArray(2);
    glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 0, 0);
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    glBindVertexArray(0);


    glGenVertexArrays(1, &pick_vao);
    glBindVertexArray(pick_vao);

    glGenBuffers(1, &pickverticesVBO);
    glBindBuffer(GL_ARRAY_BUFFER, pickverticesVBO);
    glBufferData(GL_ARRAY_BUFFER, vertices.size()*sizeof(float), &vertices[0], GL_STATIC_DRAW);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, 0);
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    glGenBuffers(1, &colorsVBO);
    glBindBuffer(GL_ARRAY_BUFFER, colorsVBO);
    glBufferData(GL_ARRAY_BUFFER, colors.size()*sizeof(float), &colors[0], GL_STATIC_DRAW);
    glEnableVertexAttribArray(1);
    glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 0, 0);
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    glBindVertexArray(0);
}

void TerrainRenderer::LoadTerritoryEdges(){
    std::vector<float> verts;
    std::vector<float> uvs;
    num_edge_triangles = BuildNationEdgeMesh(verts, uvs);

    CreateVAO(territory_edge_vao, verts, uvs);
}

void TerrainRenderer::LoadRivers(){
    Graph rivers = gamestate->GetRivers();

    std::vector<float> verts;
    std::vector<float> uvs;
    num_river_segments = BuildPathMesh(rivers, verts, uvs);

    CreateVAO(river_vao, verts, uvs);
}

void TerrainRenderer::LoadRoads(){
    Graph roads = gamestate->GetRoads();

    std::vector<float> verts;
    std::vector<float> uvs;
    num_road_segments = BuildPathMesh(roads, verts, uvs);

    CreateVAO(road_vao, verts, uvs);
}

int TerrainRenderer::BuildPathMesh(Graph graph, std::vector<float> & verts, std::vector<float> & uvs){
    int num_segments = 0;

    for(const auto & edge:graph.GetAllEdges()){
        std::vector<float> points;

        int a, b;
        std::tie(a, b, std::ignore) = edge;

        int x = gamestate->xComponent(a);
        int x2 = gamestate->xComponent(b);
        int dx = x2-x;
        int y = gamestate->yComponent(a);
        int y2 = gamestate->yComponent(b);
        int dy = y2-y;

        float angle = atan2(dx, dy);
        float length = (int)(angle*180/3.14)%90==0 ? 1 : 1.4;

        points.push_back(-0.1);
        points.push_back(0);
        points.push_back(0.1);
        points.push_back(1);

        points.push_back(-0.1);
        points.push_back(length);
        points.push_back(0.1);
        points.push_back(1);

        points.push_back(0.1);
        points.push_back(length);
        points.push_back(0.1);
        points.push_back(1);

        points.push_back(0.1);
        points.push_back(0);
        points.push_back(0.1);
        points.push_back(1);


        for(unsigned int p=0;p<points.size();p+=4){
            float xtemp = points.at(p);
            float ytemp = points.at(p+1);
            points.at(p) = -(cos(angle)*xtemp - sin(angle)*ytemp) + x;
            points.at(p+1) = sin(angle)*xtemp + cos(angle)*ytemp + y;
        }
        verts.insert(verts.begin(), points.begin(), points.end());

        uvs.push_back(0);
        uvs.push_back(0);

        uvs.push_back(0);
        uvs.push_back(1);

        uvs.push_back(1);
        uvs.push_back(1);

        uvs.push_back(1);
        uvs.push_back(0);

        num_segments++;
    }
    return num_segments;
}

int TerrainRenderer::BuildNationEdgeMesh(std::vector<float> & verts, std::vector<float> & uvs){
    int count = 0;
    std::map<int, std::set<int>> provinces = gamestate->GetAllProvinces();


    for(std::map<int, std::set<int>>::iterator iter = provinces.begin(); iter != provinces.end(); iter++){
        if(iter->first == -1){
            continue;
        }
        std::set<int> owned_tile_indices = iter->second;

        for(int index : owned_tile_indices){
            int width = gamestate->MapWidth();
            int x = gamestate->xComponent(index);
            int y = gamestate->yComponent(index);

            std::vector<int> neighbours {index-1, index+1, index-width, index+width};

            if(std::find(owned_tile_indices.begin(), owned_tile_indices.end(), index-1) == owned_tile_indices.end()){
                AddEdge(verts, uvs, x, y, 1);
                count++;
            }
            if(std::find(owned_tile_indices.begin(), owned_tile_indices.end(), index+1) == owned_tile_indices.end()){
                AddEdge(verts, uvs, x, y, 3);
                count++;
            }
            if(std::find(owned_tile_indices.begin(), owned_tile_indices.end(), index+width) == owned_tile_indices.end()){
                AddEdge(verts, uvs, x, y, 0);
                count++;
            }
            if(std::find(owned_tile_indices.begin(), owned_tile_indices.end(), index-width) == owned_tile_indices.end()){
                AddEdge(verts, uvs, x, y, 2);
                count++;
            }
        }
    }
    return 4*count; // number of triangles = 4*num of edges
}

void TerrainRenderer::AddEdge(std::vector<float> & verts, std::vector<float> & uvs, int x, int y, int rot){
    std::vector<int> order {0, 1, 5,
                            1, 5, 2,
                            5, 4, 2,
                            2, 3, 4};

    std::vector<std::tuple<float, float>> points;
    points.push_back(std::make_tuple(-0.5, 0.5));
    points.push_back(std::make_tuple(-0.45, 0.45));
    points.push_back(std::make_tuple( 0.45, 0.45));
    points.push_back(std::make_tuple( 0.5, 0.5));
    points.push_back(std::make_tuple( 0.45, 0.55));
    points.push_back(std::make_tuple(-0.45, 0.55));

    for(int i : order){
        float xoff = std::get<0>(points.at(i));
        float yoff = std::get<1>(points.at(i));

        switch(rot){
            case 0:{
                break;
            }
            case 1:{
                float temp = xoff;
                xoff = - yoff;
                yoff = temp;
                break;
            }
            case 2:{
                xoff = -xoff;
                yoff = -yoff;
                break;
            }
            case 3:{
                float temp = -xoff;
                xoff = yoff;
                yoff = temp;
                break;
            }
            default:{
                break;
            }
        }
        verts.push_back(x + xoff);
        verts.push_back(y + yoff);
        verts.push_back(0.05);
        verts.push_back(1);
    }
    for(unsigned int i; i<4; i++){
        uvs.push_back(0);
        uvs.push_back(1);
        uvs.push_back(1);
        uvs.push_back(1);
        uvs.push_back(0);
        uvs.push_back(0);
    }

}

void TerrainRenderer::CreateVAO(GLuint & vao, std::vector<float> & verts, std::vector<float> & uvs){
    GLuint verticesVBO;
    GLuint uvsVBO;

    glGenVertexArrays(1, &vao);
    glBindVertexArray(vao);

    glGenBuffers(1, &verticesVBO);
    glBindBuffer(GL_ARRAY_BUFFER, verticesVBO);
    glBufferData(GL_ARRAY_BUFFER, verts.size()*sizeof(float), &verts[0], GL_STATIC_DRAW);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, 0);

    glGenBuffers(1, &uvsVBO);
    glBindBuffer(GL_ARRAY_BUFFER, uvsVBO);
    glBufferData(GL_ARRAY_BUFFER, uvs.size()*sizeof(float), &uvs[0], GL_STATIC_DRAW);
    glEnableVertexAttribArray(2);
    glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 0, 0);
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    glBindVertexArray(0);
}

void TerrainRenderer::Draw(){
    std::shared_ptr<Shader> shader = GetShader("terrain");
    float cameraPos[] = {camx, camy, camz};

    if(shader==nullptr){
        std::cout<<"Shader name invalid\n";
        return;
    }

    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, superTexture);
    glBindSampler(1, sampler);

	glUseProgram(shader->GetProgram());

    shader->SetUniform("camera", 3, cameraPos);
    shader->SetUniform("angle", 1, &angle);
    shader->SetUniform("perspectiveMatrix", 16, perspectiveMatrix);

    glBindVertexArray(terrainDrawVAO);
    glDrawArrays(GL_QUADS, 0, 4*gamestate->NumTiles());
    glBindVertexArray(0);

    glBindTexture(GL_TEXTURE_2D, resourceManager->GetTexture("river"));
    glBindVertexArray(river_vao);
    glDrawArrays(GL_QUADS, 0, 4*num_river_segments);
    glBindVertexArray(0);

    glBindTexture(GL_TEXTURE_2D, resourceManager->GetTexture("road"));
    glBindVertexArray(road_vao);
    glDrawArrays(GL_QUADS, 0, 4*num_road_segments);
    glBindVertexArray(0);

    glBindTexture(GL_TEXTURE_2D, resourceManager->GetTexture("road"));
    glBindVertexArray(territory_edge_vao);
    glDrawArrays(GL_TRIANGLES, 0, 3*num_edge_triangles);
    glBindVertexArray(0);

	glUseProgram(0);

	glBindSampler(1, 0);
}

void TerrainRenderer::Pick(){
    std::shared_ptr<Shader> shader = GetShader("terrain_picking");
    float cameraPos[] = {camx, camy, camz};

    if(shader==nullptr){
        std::cout<<"Shader name invalid\n";
        return;
    }

	glUseProgram(shader->GetProgram());

    shader->SetUniform("camera", 3, cameraPos);
    shader->SetUniform("angle", 1, &angle);
    shader->SetUniform("perspectiveMatrix", 16, perspectiveMatrix);

    glBindVertexArray(pick_vao);

    glDrawArrays(GL_QUADS, 0, 4*gamestate->NumTiles());
    glBindVertexArray(0);

	glUseProgram(0);
}
